#include "FtpServer.h"
#include "Settings.h"

#include <QApplication>
#include <QFileInfo>
#include <QDir>
#include <QNetworkConfigurationManager>
#include <QDebug>

#include <QSslCertificate>
#include <QSslKey>
#include <QDateTime>

FtpServer::FtpServer(QObject *parent)
    : QObject(parent)
    , m_process(0)
    , m_autoOnline(true)
    , m_starting(false)
{
    m_networkConfigManager = new QNetworkConfigurationManager(this);
    m_networkSession = 0;

    slotSettingsValueChanged(QLatin1String("autoOnline"));
    connect(Settings::instance(), SIGNAL(valueChanged(QString)), this, SLOT(slotSettingsValueChanged(QString)));

    connect(m_networkConfigManager, SIGNAL(onlineStateChanged(bool)), this, SLOT(slotOnlineChanged()));
    connect(m_networkConfigManager, SIGNAL(updateCompleted()), this, SLOT(slotNetworkUpdated()));
}

FtpServer::~FtpServer()
{
    if (m_process && m_process->isOpen()) {
        m_starting = false;
        m_process->kill();
        m_process->waitForFinished();
    }
}

FtpServer* FtpServer::instance()
{
    static FtpServer *ftpserver = 0;
    if (!ftpserver)
        ftpserver = new FtpServer();
    return ftpserver;
}

bool FtpServer::autoOnline() const
{
    return m_autoOnline;
}

void FtpServer::setAutoOnline(bool enable)
{
    if (m_autoOnline == enable)
        return;
    m_autoOnline = enable;
    Settings::instance()->setValue(QLatin1String("autoOnline"), m_autoOnline);
}

QString FtpServer::pythonExecutable()
{
    if (m_pythonExecutable.isEmpty()) {
        QProcess p;
        p.start(QLatin1String("which python"));
        p.waitForFinished();
        if (p.exitCode() == 0) {
            m_pythonExecutable = p.readAll().trimmed();
        }
    }
    return m_pythonExecutable;
}

QString FtpServer::serverExecutable()
{
    if (m_serverExecutable.isEmpty()) {
        QStringList args = QApplication::arguments();
        if (args.isEmpty()) {
            qDebug() << "FtpServer::serverExecutable No arguments";
            return QString();
        }
        QFileInfo fi(args.first());
        QDir dir = fi.absoluteDir();
        if (!dir.cd(QLatin1String("server"))) {
            if (!dir.cdUp() || !dir.cd(QLatin1String("server"))) {
                qDebug() << "FtpServer::serverExecutable No server directory";
                return QString();
            }
        }
        QFileInfo file(dir, "server.py");
        if (!file.exists()) {
            qDebug() << "FtpServer::serverExecutable No server.py";
            return QString();
        }
        m_serverExecutable = file.absoluteFilePath();
    }
    return m_serverExecutable;
}

bool FtpServer::hasPemCertificate()
{
    return !pemCertificate().isEmpty();
}

QByteArray FtpServer::pemCertificate()
{
    QByteArray cert = Settings::instance()->value("SslCertificate").toByteArray();
    if (cert.isNull()) { // No pem-certificate yet. Let's generate a self-signed one right now.

        // Generate the private key
        QProcess p;
        p.start(QLatin1String("openssl genrsa 1024"));
        p.waitForFinished();
        if (p.exitCode() != 0) {
            qWarning() << "FtpServer::pemCertificate Failed to call 'openssl genrsa'";
            return QByteArray();
        }
        QByteArray privkey = p.readAllStandardOutput();

        // Generate the pem-certificate
        p.start(QLatin1String("openssl req -new -x509 -nodes -sha1 -batch -days 3650 -keyout /dev/stdout"));
        p.write(privkey);
        p.waitForFinished();
        if (p.exitCode() != 0) {
            qWarning() << "FtpServer::pemCertificate Failed to call 'openssl req'";
            return QByteArray();
        }
        cert = p.readAllStandardOutput();

        // Verify that the result is valid using QSsl
        QSslCertificate sslcert(cert);
        if (!sslcert.isValid() || sslcert.isNull() || sslcert.publicKey().isNull()) {
            qWarning() << "FtpServer::pemCertificate Failed to create a valid pem-certificate. isValid=" << sslcert.isValid() << "isNull=" << sslcert.isNull() << "publicKey.isNull=" << sslcert.publicKey().isNull() << "cert.isEmpty=" << cert.isEmpty();
            return QByteArray();
        }
        qDebug() << "FtpServer::pemCertificate Successfully created valid pem-certificate with serialNumber=" << sslcert.serialNumber();

        // Encode as hex before saving and returning the result
        cert = cert.toHex();
        // Write the certificate to our config-file so next time we don't regenerate but pick the same cert up again
        Settings::instance()->setValue("SslCertificate", cert);
        // Explicit ask to save all changes right now. That prevents that on later crashes we would lose the certificate again
        Settings::instance()->saveChanges();
    }
    return cert;
}

bool FtpServer::start()
{
    qDebug() << "FtpServer::start";

    if (!m_process) {
        m_process = new QProcess(this);
        m_process->setProcessChannelMode(QProcess::MergedChannels);
        //m_process->setProcessChannelMode(QProcess::ForwardedChannels);
        connect(m_process, SIGNAL(started()), this, SIGNAL(started()));
        connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(slotError(QProcess::ProcessError)));
        connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SIGNAL(stopped()));
        connect(m_process, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(slotStateChanged(QProcess::ProcessState)));
        connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(slotReadyReadStandardError()));
        connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReadStandardOutput()));
        connect(m_process, SIGNAL(readyRead()), this, SLOT(slotReadyRead()));
    }

    QString program = pythonExecutable();
    if (program.isEmpty()) {
        emit output(tr("Python-executable not found. Please install python."));
        emit stopped();
        return false;
    }

    QString executable = serverExecutable();
    if (executable.isEmpty() || !QFile(executable).exists()) {
        emit output(tr("Server-executable not found. This is an installation issue."));
        emit stopped();
        return false;
    }

    m_rootDir = Settings::instance()->value("serverRootDir").toString();
    if (m_rootDir.startsWith("~")) {
        m_rootDir = m_rootDir.mid(1);
        if (m_rootDir.startsWith(QDir::separator()))
            m_rootDir = m_rootDir.mid(1);
        QDir d(QDir::home());
        if (!d.exists()) {
            emit output(QString("Homedir does not exist"));
            emit stopped();
            return false;
        }
        if (m_rootDir.isEmpty() || d.cd(m_rootDir))
            m_rootDir = d.absolutePath();
        else
            m_rootDir.clear();
    }
    if (m_rootDir.isEmpty() || !QDir(m_rootDir).exists()) {
        qDebug() << "FtpServer::start Invalid rootdir=" << m_rootDir;
        emit output(QString("Rootdir does not exist"));
        emit stopped();
        return false;
    }

    // Do not set the working directory cause we need QApplication::arguments[0] to be absolute.
    //m_process->setWorkingDirectory(m_rootDir);

    QStringList arguments;
    arguments << executable;
    arguments << "--stdin=1"; // read config from stdin

    qDebug() << "Execute program=" << program << "arguments=" << arguments;
    m_starting = true;
    m_process->start(program, arguments, QIODevice::ReadWrite);
    return true;
}

void FtpServer::stop()
{
    qDebug() << "FtpServer::stop";
    m_starting = false;
    if (!m_process)
        return;
    m_process->kill();
}

void FtpServer::slotError(QProcess::ProcessError err)
{
    if (!m_starting)
        return;
    qDebug() << "FtpServer::slotError error=" << err;
    switch (err) {
        case QProcess::FailedToStart:
            emit output(tr("The process failed to start. Either the invoked program is missing, or you may have insufficient permissions to invoke the program."));
            break;
        case QProcess::Crashed:
            emit output(tr("The process crashed some time after starting successfully."));
            break;
        case QProcess::Timedout:
            emit output(tr("Execution timed out."));
            break;
        case QProcess::WriteError:
            emit output(tr("An error occurred when attempting to write to the process."));
            break;
        case QProcess::ReadError:
            emit output(tr("An error occurred when attempting to read from the process."));
            break;
        case QProcess::UnknownError:
            emit output(tr("An unknown error occurred."));
            break;
    }
}

void FtpServer::slotStateChanged(QProcess::ProcessState state)
{
    QString s;
    switch (state) {
        case QProcess::NotRunning: s = "stopped"; break;
        case QProcess::Starting: s = "starting"; break;
        case QProcess::Running:  s = "running"; break;
    }
    qDebug() << "FtpServer::slotStateChanged state=" << s;

    switch (state) {
        case QProcess::NotRunning: {
            if (m_networkSession && m_networkSession->isOpen())
                m_networkSession->close();
            break;
        }
        case QProcess::Starting: {
            break;
        }
        case QProcess::Running: {
            QStringList arguments;
            arguments << QString("address=%1").arg(Settings::instance()->value("serverAddress").toString());
            arguments << QString("port=%1").arg(Settings::instance()->value("serverPort").toString());
            arguments << QString("rootdir=%1").arg(m_rootDir);

            QString username = Settings::instance()->value("serverUsername").toString();
            if (!username.isEmpty()) {
                arguments << QString("username=%1").arg(username);
                QString userpass = Settings::instance()->value("serverUserpass").toString();
                if (!userpass.isEmpty())
                    arguments << QString("userpass=%1").arg(userpass);
            }

            if (Settings::instance()->value("serverAllowAnonymous").toBool())
                arguments << QString("allow_anon=1");

            if (Settings::instance()->value("serverTls").toBool()) {
                QByteArray cert = pemCertificate();
                if (cert.isEmpty()) {
                    emit output(QString("Failed to create a valid SFTP-certificate"));
                    m_process->kill();
                    return;
                }
                arguments << QString("tlspem=%1").arg(QString::fromUtf8(cert));
            }

            //arguments << QString("max_cons=%1").arg();
            //arguments << QString("max_cons_per_ip=%1").arg();

            Q_FOREACH(const QString &arg, arguments)
                m_process->write(arg.toUtf8() + "\n");
            m_process->closeWriteChannel();

            if (m_autoOnline) {
                if (!m_networkSession) {
                    QNetworkConfiguration networkConfig = m_networkConfigManager->defaultConfiguration();
                    if (!networkConfig.isValid()) {
                        qDebug() << "Default network configuration invalid. Cannot go online.";
                    } else {
                        // Note that we only create the QNetworkSession once and then later adapt if the configuration
                        // changes. This is the prefered way and it seems to be a bad idea to delete and recreate the
                        // QNetworkSession instance manually since that can have all kind of "funny" side-effects
                        // including a stack-corruption on the N9 with Pr1.2 :-/
                        m_networkSession = new QNetworkSession(networkConfig, m_networkConfigManager);
                        connect(m_networkSession, SIGNAL(opened()), this, SLOT(slotSessionOpened()));
                        connect(m_networkSession, SIGNAL(closed()), this, SLOT(slotSessionClosed()));
                        connect(m_networkSession, SIGNAL(stateChanged(QNetworkSession::State)), this, SLOT(slotSessionStateChanged(QNetworkSession::State)));
                        connect(m_networkSession, SIGNAL(error(QNetworkSession::SessionError)), this, SLOT(slotSessionError(QNetworkSession::SessionError)));
                        connect(m_networkSession, SIGNAL(newConfigurationActivated()), this, SLOT(slotSessionNewActivated()));
                        connect(m_networkSession, SIGNAL(preferredConfigurationChanged(QNetworkConfiguration,bool)), this, SLOT(slotSessionPreferredChanged(QNetworkConfiguration,bool)));
                    }
                }
                if (m_networkSession)
                    m_networkSession->open();
            }
            break;
        }
    }
}

void FtpServer::slotReadyReadStandardError()
{
    QByteArray err = m_process->readAllStandardError();
    qDebug() << "FtpServer::slotReadyReadStandardError error=" << err;
    emit output(QString(err));
}

void FtpServer::slotReadStandardOutput()
{
    QByteArray out = m_process->readAllStandardOutput();
    qDebug() << "FtpServer::slotReadStandardOutput out=" << out;
    emit output(QString(out));
}

void FtpServer::slotReadyRead()
{
    /*
    if (!m_process->canReadLine())
        return;
    QByteArray line = m_process->readLine();
    qDebug() << "FtpServer::slotReadyRead line=" << line;
    */
}

void FtpServer::slotSettingsValueChanged(const QString &name)
{
    if (name.isEmpty() || name == QLatin1String("autoOnline")) {
        bool autoOnline = Settings::instance()->value(QLatin1String("autoOnline"), m_autoOnline).toBool();
        if (autoOnline != m_autoOnline) {
            m_autoOnline = autoOnline;
            qDebug() << "FtpServer::slotSettingsValueChanged The value for autoOnline changed to=" << m_autoOnline;
        }
    }
}

void FtpServer::slotOnlineChanged()
{
    qDebug() << "Online state changed, isOnline=" << m_networkConfigManager->isOnline() << "isRunning=" << m_process->isOpen() << "autoOnline=" << m_autoOnline;
}

void FtpServer::slotNetworkUpdated()
{
    qDebug() << "FtpServer::slotNetworkUpdated";
}

void FtpServer::slotSessionOpened()
{
    qDebug() << "FtpServer::slotSessionOpened";
}

void FtpServer::slotSessionClosed()
{
    qDebug() << "FtpServer::slotSessionClosed";
}

void FtpServer::slotSessionStateChanged(QNetworkSession::State state)
{
    qDebug() << "FtpServer::slotSessionStateChanged state=" << state;
}

void FtpServer::slotSessionError(QNetworkSession::SessionError error)
{
    qDebug() << "FtpServer::slotSessionError error=" << error;
    //stop();
}

void FtpServer::slotSessionNewActivated()
{
    qDebug() << "FtpServer::slotSessionNewActivated";
    m_networkSession->accept();
}

void FtpServer::slotSessionPreferredChanged(const QNetworkConfiguration &config, bool isSeamless)
{
    if (config.isValid())
        qDebug() << "FtpServer::slotSessionPreferredChanged configBearer=" << config.bearerName() << "configIdentifier=" << config.identifier() << "configName=" << config.name() << "isSeamless=" << isSeamless;
    else
        qDebug() << "FtpServer::slotSessionPreferredChanged Invalid network configuration - isSeamless=" << isSeamless;
    //stop();
    m_networkSession->migrate();
}
